<?php
namespace bdhert\PhpLock;

use bdhert\PhpLock\exception\DriverException;
use Predis\Client;

/**
 * redis实现分布式锁
 * Class RedisLock
 * @package bdhert\PhpLock
 */
class RedisLock implements ILock{
    protected ?Client $driver;

    public function __construct(Client $redis) {
        $this->driver = $redis;
    }

    /**
     * 获取锁
     * @param $key
     * @param int $timeout
     * @return bool
     * @throws \Exception
     */
    public function get($key, $timeout = self::EXPIRE){
        if(strlen($key) === 0) return false;

        $start = $this->microTime();
        $times = 0;

        try {
            do{
                $times++;
                if($acquired = $this->driver->set(
                    "lock:{$key}",
                    1,
                    'PX',
                    self::LOCK_WAIT,
                    'NX'
                )) break;

                if((int)$timeout === 0) break;

                usleep(self::LOCK_SLEEP);
            } while (!is_numeric($timeout) || ($this->microTime() <= ($start + $timeout * 1000000)));
        }catch (\Exception $e){
            throw new DriverException('redis锁操作错误', 500);
        }

        // 时间差补足 如补足读写分离的延迟阻塞 已补足第一次拿锁不补足时间差的漏洞 充分保证业务逻辑的有效性与及时响应 此锁误伤率大
        $is_diff = false;
        if($acquired){
            $is_diff   = ($times > 1);
            $lock_diff = (int)ceil(self::LOCK_DIFF / 1000);

            if($lock_diff && !($diff_res = $this->driver->set(
                "lock:diff:{$key}",
                1,
                'PX',
                $lock_diff,
                'NX'
            ))) $is_diff = true;
        }
        if($is_diff) usleep(self::LOCK_DIFF);

        return $acquired ? true : false;
    }

    /**
     * 释放锁
     * @param $key
     */
    public function release($key){
        if(strlen($key) !== 0){
            $this->driver->del("lock:$key");
        }
    }

    /**
     * 获取当前微秒
     * @return int
     */
    private function microTime(){
        return (int)bcmul(microtime(true), 1000000);
    }
}
